﻿//*** Guidance generated code ***//
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;

using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;
using NHibernate.Transform;

using ProjectBase.Core;

using IFilter = ProjectBase.Core.IFilter;

namespace ProjectBase.Data.NH
{
    /// <summary>
    ///     Generic Dao object, which could be used for CRUDing any type of (mapped) .NET object to DB
    /// </summary>
    /// <typeparam name = "TEntity">The object type</typeparam>
    /// <typeparam name = "TID">Type of ID property</typeparam>
    public abstract class NHGenericDao<TEntity, TID> : IWriteDao<TEntity, TID>
        where TEntity : class, IPersistentObjectWithTypedId<TID>
    {
        #region members
        ICriteria _criteria;
        #endregion members

        #region IReadDao<T,TID>
        /// <summary>
        ///     NHibernate ISession is asked for unique entity 'T' with provided ID
        /// </summary>
        public virtual TEntity GetById(TID id)
        {
            return Session.Get<TEntity>(id);
        }

        /// <summary>
        ///     Allows filtering results. Using NHibernate criteria (similar to LINQ).
        ///     The result set is filtered by all criteria, which were appended to the Crieteria property
        /// </summary>
        /// <param name = "filter"></param>
        /// <returns></returns>
        protected virtual IEnumerable<TEntity> GetListByCriteria(IFilter filter)
        {
            // Clone clean criteria to get TOTAL count
            var totalCountCriteria = CriteriaTransformer.TransformToRowCount(Criteria);
            
            // Paging
            int skipResultsCount = filter.CurrentPage * filter.MaxRowsPerPage;
            int maxRowsCount = filter.MaxRowsPerPage;
            //var totalRowCount = AdjustPaging(filter, out skipResultsCount, out maxRowsCount);

            // apply Paging
            Criteria.SetFirstResult(skipResultsCount);
            Criteria.SetMaxResults(maxRowsCount); // set count limits for result list

            // apply caches
            Criteria.SetCacheable(true)
                .SetCacheMode(CacheMode.Normal)
                .SetCacheRegion("LongTerm");

            // apply order
            if (filter.OrderBy.Is())
            {
                AddOrder(filter.OrderBy, Criteria);
            }

            // batch SQL query for List and COUNT(*)
            var list = FindList(filter, Criteria);
            filter.TotalRowCount = GetTotalRowCount(totalCountCriteria);

            // in unlikely case that the totalCount is less then expected
            // and list is empty, rewind at the beginig
            if (list.IsEmpty()
              && filter.TotalRowCount < skipResultsCount)
            {
                Criteria.SetFirstResult(0);
                filter.CurrentPage = 0;
                list = Criteria.List<TEntity>();
            }

            _criteria = null; // clear the criteria before another usage
            return list;
        }

        static int GetTotalRowCount(ICriteria DefaultFactory)
        {
            // use the same criteria to get projection for COUNT(*)
            DefaultFactory.SetCacheable(true)
                .SetCacheMode(CacheMode.Normal)
                .SetCacheRegion("LongTerm");
            return DefaultFactory
                .FutureValue<int>()
                .Value;
        }
        #endregion IReadDao<T,TID> Get

        #region IWriteDao<T,TID> Set
        /// <summary>
        ///     For entities that have assigned ID's, you must explicitly call Add to add a new one.
        ///     See http://www.hibernate.org/hib_docs/nhibernate/html_single/#mapping-declaration-id-assigned.
        /// </summary>
        public virtual TEntity Add(TEntity entity)
        {
            Session.Save(entity);
            return entity;
        }

        /// <summary>
        ///     For entities that have assigned ID's, you should explicitly call Update to update an existing one.
        ///     Updating also allows you to commit changes to a detached object.  More info may be found at:
        ///     http://www.hibernate.org/hib_docs/nhibernate/html_single/#manipulatingdata-updating-detached
        /// </summary>
        public virtual TEntity Update(TEntity entity)
        {
            Session.Update(entity);
            return entity;
        }

        /// <summary>
        ///     ITracked entities in TrackedMode are only made expired.
        ///     In other cases the record is delted from DB
        /// </summary>
        /// <param name = "entity"></param>
        public virtual void Delete(TEntity entity)
        {
            Session.Delete(entity);
        }
        #endregion IWriteDao<T,TID> Set

        #region protected MaxId
        protected virtual TID GetMaxId()
        {
            return Session
                .CreateCriteria(typeof(TEntity))
                .AddOrder(new Order(Constants.Common.ID, false))
                .SetProjection(Projections.Property(Constants.Common.ID))
                .SetMaxResults(1)
                .UniqueResult<TID>();
        }
        #endregion protected MaxId

        #region protected static - filter support
        /// <summary>
        ///     Creates order criteria from the IFilter object OrderBy property.
        ///     If there are already any Orders, method returns
        /// </summary>
        protected static void AddOrder(IDictionary<string, bool> orderBy, ICriteria criteria)
        {
            Contract.Requires(criteria.Is(), "Cannot create order by clause on ICriteria criteria == null");
            criteria.ClearOrders();
            foreach (var pair in orderBy)
            {
                if (pair.Key.Contains(".")) // ordering object contains inner objects
                {
                    var name = pair.Key;
                    CreateCriteria(ref name, criteria).AddOrder(new Order(name, pair.Value));
                }
                else
                {
                    criteria.AddOrder(new Order(pair.Key, pair.Value));
                }
            }
        }

        /// <summary>
        ///     If there is any item in the PropertyList - projection is returned.
        ///     It allows effectively select only some columns for large entities
        ///     Else the standard Criteria.List().
        /// </summary>
        /// <param name = "filterObject"></param>
        /// <param name = "criteria"></param>
        /// <returns></returns>
        protected static IEnumerable<TEntity> FindList(IFilter filterObject, ICriteria criteria)
        {
            if (filterObject.PropertyList.Is()
                && filterObject.PropertyList.Count > 0)
            {
                // I. ID is needed anyway
                filterObject.PropertyList["ID"] = "ID";

                // II. Declare the propertyList which will be filled and aliases
                var propertyList = Projections.ProjectionList();

                // III. one by one the path of filtered property is appended to the list + it's Alias is created
                foreach (var pair in filterObject.PropertyList)
                {
                    if (pair.Key.IsEmpty())
                    {
                        throw new DaoException("Projection: Cannot process collection or projections 'filter.PropertyList' with any null or empty key");
                    }
                    var name = pair.Key;
                    var alias = pair.Value ?? name;
                    // IV. Inner Entity property needs special handling
                    if (name.Contains("."))
                    {
                        if (pair.Value.IsEmpty())
                        {
                            throw new DaoException("Projection: The virtual property name is missing. For projections of inner types e.g.({0}) there must be selected some entity's property to be used as a SELECT target".FormatWith(pair.Key));
                        }
                        CreateCriteria(ref name, criteria);
                    }
                    // V. finally let's append the property to the list
                    propertyList.Add(Projections.Property(name), alias);
                }

                filterObject.PropertyList = null; //.Clear(); // no need to store it anymore. Fill it again if needed

                // VI. really smart select is applied for List purposes
                criteria
                    .SetProjection(propertyList)
                    .SetResultTransformer(new AliasToBeanResultTransformer(typeof(TEntity)))
                    //.SetCacheable(false);
                    .SetCacheable(true)
                    .SetCacheMode(CacheMode.Normal)
                    .SetCacheRegion("LongTerm");
            }
            // VII. simple situation (but with complete SQL Select statement - pure entity list
            return criteria.Future<TEntity>();
        }

        /// <summary>
        ///     Supports
        ///     1) converting the PropertyList to correct Projection List.
        ///     2) adding Orders
        ///     Searches existing criteria to append order or use the alias
        /// </summary>
        /// <param name = "name">name incoming as Subject.Address.City.ID and outgoing as Subject_Address_City.ID</param>
        /// <param name = "criteria">usually the base Criteria (not SubCriteria)</param>
        /// <returns>criteria based on the name</returns>
        protected static ICriteria CreateCriteria(ref string name, ICriteria criteria)
        {
            Contract.Requires(name.IsNotEmpty(),
                "CreateAlias can be called ONLY for a path containing '.' as entities path separator");
            var keys = name.Split('.');

            var plus = string.Empty;
            var alias = string.Empty;
            int i;

            for (i = 0; i < keys.Length - 1; i++)
            {
                string associationPath = keys[i];
                alias += plus + associationPath;
                var subCriteria = criteria.GetCriteriaByPath(associationPath);
                if (subCriteria.IsNull())
                {
                    subCriteria = criteria.GetCriteriaByAlias(alias);
                    if (subCriteria.IsNull())
                    {
                        subCriteria = criteria.CreateCriteria(associationPath, alias);
                    }
                }
                else
                {
                    if (subCriteria.Alias.IsEmpty())
                    {
                        throw new DaoException("Criteria: SubCriteria without Alias found - cannot proceed."
                                                              + " The criteria with path '"
                                                              + ((CriteriaImpl.Subcriteria)subCriteria).Path
                                                              + "' must have set an 'alias'. "
                                                              +
                                                              " Suggestion: DO USE extension ICriteria.CreateSubCriteria(associationPath) ");
                    }
                    alias = subCriteria.Alias;
                }
                criteria = subCriteria;
                plus = "_";
            }
            name = alias + "." + keys[i];
            return criteria;
        }
        #endregion protected static - filter support

        #region properties
        /// <summary>
        ///     Gateway to the DB
        /// </summary>
        protected ISession Session
        {
            get { return NHSession.GetCurrent(SessionFactoryName); }
        }

        /// <summary>
        ///     Object for filtering. Appending the criteria will allow to select more precious results.
        /// </summary>
        protected ICriteria Criteria
        {
            get
            {
                if (_criteria.IsNull())
                {
                    _criteria = Session.CreateCriteria(typeof(TEntity));
                }
                return _criteria;
            }
        }

        protected virtual string SessionFactoryName
        {
            get { return Const.DefaultFactory; }
        }
        #endregion properties
    }
}